/*
 * Copyright (C) 2010 Eric Bottard
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.excilys.oss.dao.dynamicfinders.hibernate;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;

import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Restrictions;

import com.excilys.oss.dao.dynamicfinders.DataAccessObject;
import com.excilys.oss.dao.dynamicfinders.FinderDescriptor;
import com.excilys.oss.dao.dynamicfinders.PropertyAndComparator;

/**
 * An {@link InvocationHandler} that uses the Hibernate {@link Criteria} API to
 * query the database. Needs a {@link SessionFactory} at creation time (given by
 * the {@link HibernateDynamicFinderFactory}.
 * 
 * @author Eric Bottard
 * 
 * @param <I>
 *            the DAO interface type. Unused at compile time, in fact.
 */
public class HibernateCriteriaDynamicFinderInvocationHandler<I> implements
		InvocationHandler {

	private final Class<?> entityClass;

	private final Map<Method, FinderDescriptor> descriptors;

	private SessionFactory sessionFactory;

	public HibernateCriteriaDynamicFinderInvocationHandler(Class<I> itf,
			Map<Method, FinderDescriptor> descriptors,
			SessionFactory sessionFactory) {
		this.descriptors = descriptors;
		this.sessionFactory = sessionFactory;

		DataAccessObject daoAnno = itf.getAnnotation(DataAccessObject.class);
		if (daoAnno != null) {
			Class<?> annoEntityClass = daoAnno.entityType();
			this.entityClass = annoEntityClass != Object.class ? annoEntityClass
					: null;
		} else {
			this.entityClass = null; // todo : could be guessed from one method
			// signature
		}

	}

	/*
	 * Finds the descriptor for the method being invoked and does a
	 * straightforward mapping from PropertyAndComparator to Hibernate Criteria
	 * concepts.
	 */
	public Object invoke(Object proxy, Method method, Object[] args)
			throws Throwable {
		FinderDescriptor descriptor = descriptors.get(method);

		Criteria criteria = obtainSession().createCriteria(
				entityClassToUse(method));
		int argIndex = 0;
		for (PropertyAndComparator pac : descriptor.props) {
			criteria.add(buildRestrictionForProperty(pac, args, argIndex));
			argIndex += pac.comparator.nbArguments;
		}

		return descriptor.returnsAll ? criteria.list() : criteria
				.uniqueResult();
	}

	protected Session obtainSession() {
		return sessionFactory.getCurrentSession();
	}

	private Criterion buildRestrictionForProperty(PropertyAndComparator pac,
			Object[] args, int argOffset) {
		switch (pac.comparator) {
		case Between:
			return Restrictions.between(pac.property, args[argOffset],
					args[argOffset + 1]);
		case Equals:
			return Restrictions.eq(pac.property, args[argOffset]);
		case GreaterThan:
			return Restrictions.gt(pac.property, args[argOffset]);
		case GreaterThanEquals:
			return Restrictions.ge(pac.property, args[argOffset]);
		case Ilike:
			return Restrictions.ilike(pac.property, args[argOffset]);
		case IsNotNull:
			return Restrictions.isNotNull(pac.property);
		case IsNull:
			return Restrictions.isNull(pac.property);
		case LessThan:
			return Restrictions.lt(pac.property, args[argOffset]);
		case LessThanEquals:
			return Restrictions.le(pac.property, args[argOffset]);
		case Like:
			return Restrictions.like(pac.property, args[argOffset]);
		case NotEqual:
			return Restrictions.ne(pac.property, args[argOffset]);
		default:
			throw new IllegalStateException("Should not happen");
		}
	}

	protected Class entityClassToUse(Method method) {
		return this.entityClass;
	}

}
